package com.xored.searchwt;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Pattern;

import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.part.MultiPageEditorPart;

import com.xored.searchwt.highlight.EditorAnnotationManager;
import com.xored.searchwt.highlight.IMatchesListener;
import com.xored.searchwt.highlight.Match;
import com.xored.searchwt.highlight.SearchEngine;

/**
 * Represents the search agent. It tracks the active
 * part and retargets when necessary. The find/replace
 * target is retrieved from the active part using
 * <code>getAdapter(IFindReplaceTarget.class)</code>
 * <p>
 * The agent has the same life cycle as the search view.</p>
 */
public class SearchAgent {
	
	public static final String EXT = "com.xored.searchwt.";
	public static final String HIGHLIGHT = EXT + "highlight";
	public static final String MATCH_CASE = EXT + "match_case";
	public static final String PREFIX = EXT + "prefix";
	
	/** Searching from the current position */
	public static final int FIND_HERE = 0;
	/** Searching next occurrence of the string */
	public static final int FIND_NEXT = 1;
	/** Searching previous occurrence of the string */
	public static final int FIND_PREVIOUS = 2;

	/**
	 * Creates a new search agent at the given search view.
	 * 
	 * @param view given search view
	 */
	public SearchAgent(ISearchView view) {
		this.view = view;
		engine = new SearchEngine();
		engine.start();
	}
	
	public void init() {
		agentListener = new AgentListener();
		Shell shell = view.getShell();
		if (shell != null)
			shell.addDisposeListener(agentListener);
		fWindow = view.getWindow();
		if (fWindow != null) {
			IPartService service= fWindow.getPartService();
			service.addPartListener(agentListener);
			agentListener.partActivated(service.getActivePart(), true);
		}
	}
	
	public static boolean isCanBeActivated(ISearchView view) {
		IWorkbenchWindow window = view.getWindow();
		if (window != null) {
			IPartService service= window.getPartService();
			IWorkbenchPart part = service.getActivePart();
			if (part != null) {
				IFindReplaceTarget target = (IFindReplaceTarget) part.getAdapter(IFindReplaceTarget.class);
				return target != null;
			}
		}
		return false;
	}
	
	public void set(String param, boolean enabled) {
		if (HIGHLIGHT.equals(param))
			setHighlight(enabled);
		else if (MATCH_CASE.equals(param))
			setMatchCase(enabled);
		else if (PREFIX.equals(param))
			setPrefix(enabled);
	}
	
	/**
	 * Set case sensitive search
	 * 
	 * @param matchCase true performs a case sensitive search, false an insensitive search
	 */
	public void setMatchCase(boolean matchCase) {
		this.matchCase = matchCase;
		updateHighlight(false);
	}
	
	/**
	 * Set word prefix search
	 * 
	 * @param prefix if true only occurrences are reported in which the string stands as a word by itself
	 */
	public void setPrefix(boolean prefix) {
		this.prefix = prefix;
		updateHighlight(false);
	}
	
	public void setHighlight(boolean highlight) {
		this.highlight = highlight;
		updateHighlight(false);
	}
	
	/**
	 * Perform search of the given text in a current target
	 * 
	 * @param text the string which should be found
	 * @param type one of the <code>FIND_HERE</code>, <code>FIND_NEXT</code> or <code>FIND_PREVIOUS</code> search type
	 * @return false if the given text did not find and true, otherwise
	 */
	public boolean find(String text, int type) {
		this.text = text;
		if (notFoundText != null && text.startsWith(notFoundText))
			return false;
		if (type == FIND_HERE)
			updateHighlight(true);
		if (target != null && target.canPerformFind()) {
			if (text == null || text.length() == 0) {
				removeSelection();
			}
			else {
				boolean forward = type != FIND_PREVIOUS;
				switch (type) {
					case FIND_NEXT:
						selectedIndex++;
						break;
					case FIND_PREVIOUS:
						selectedIndex--;
						break;
					default:
						break;
				}
				boolean needCheckFromBegin = selectedIndex != 0;
				selectedIndex = target.findAndSelect(selectedIndex, text, forward, matchCase, false);
				if (selectedIndex < 0 && needCheckFromBegin)
					selectedIndex = target.findAndSelect(-1, text, forward, matchCase, false);
				if (selectedIndex < 0) {
					removeSelection();
					selectedIndex = 0;
					notFoundText = text;
					return false;
				}
			}
		}
		return true;
	}
	
	public void dispose() {
		agentListener.widgetDisposed(null);
		agentListener = null;
		engine.exit();
		engine = null;
	}
	
	public void backFocus() {
		IWorkbenchPart part = mPart != null ? mPart : fPart;
		if (part != null) {
			IWorkbenchPartSite site = part.getSite();
			if (site != null) {
				IWorkbenchPart aPart = getActive(site);
				if (part != null) {
					//we need this hack for back selection, because we can't 
					//directly call selection service
					IWorkbenchPart copy = part;
					site.getPage().activate(aPart);
					site.getPage().activate(copy);
				}
			}
		}
	}
	
	private IWorkbenchPart getActive(IWorkbenchPartSite site) {
		IWorkbenchPage page = site.getPage();
		IViewReference[] views = page.getViewReferences();
		for (int i = 0; i < views.length; i++) {
			IViewPart view = views[i].getView(false);
			if (view != null && page.isPartVisible(view) && fPart != view)
				return view;
		}
		IEditorReference[] editors = page.getEditorReferences();
		for (int i = 0; i < editors.length; i++) {
			IEditorPart editor = editors[i].getEditor(false);
			if (editor != null && page.isPartVisible(editor) && fPart != editor)
				return editor;
		}
		return null;
	}
	
	private void updateHighlight(boolean paused) {
		if (agentListener != null && text != null) {
			if (highlight)
				agentListener.highlight(text, paused);
			else
				agentListener.highlight("", false);
		}
	}
	
	private class AgentListener implements IPartListener,
			DisposeListener, ISelectionChangedListener, IMatchesListener {
		
		private HashMap managers = new HashMap();
		private EditorAnnotationManager activeManager;
		
		/*
		 * @see IPartListener#partActivated(IWorkbenchPart)
		 */
		public void partActivated(IWorkbenchPart part, final boolean init) {
			if (view.ignore(part))
				return;
			
			if (part instanceof MultiPageEditorPart) {
				final MultiPageEditorPart mPart = (MultiPageEditorPart)part;
				if (SearchAgent.this.mPart != mPart)
					disposeMultiPart();
				SearchAgent.this.mPart = mPart;
				
				IEditorSite site = (IEditorSite)mPart.getEditorSite();
				ISelectionProvider provider = site.getSelectionProvider();
				
				if (provider != null) {
					provider.addSelectionChangedListener(this);
					updateTargetPart(getActiveEditor(mPart), init);
				}
			}
			else {
				disposeMultiPart();
				updateTargetPart(part, init);
			}
		}
		
		private void disposeMultiPart() {
			if (mPart != null) {
				IEditorSite site = (IEditorSite)mPart.getEditorSite();
				ISelectionProvider provider = site.getSelectionProvider();
				if (provider != null)
					provider.removeSelectionChangedListener(this);
				mPart = null;
			}
		}
		
		private void updateTargetPart(IWorkbenchPart part, boolean init) {
			IFindReplaceTarget target= part == null ? 
					null : (IFindReplaceTarget) part.getAdapter(IFindReplaceTarget.class);
			fPart= target == null ? null : part;
			if (fPreviousTarget != target) {
				fPreviousTarget= target;
				changeManager(part);
				updateTarget(target, init, activeManager != null);
			}
		}
		
		private IWorkbenchPart getActiveEditor(MultiPageEditorPart part) {
			try {
				Method method = part.getClass().getMethod("getActiveEditor", new Class[] {});
				return (IEditorPart)method.invoke(part, new Object[] {});
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}
		
		public void matched(Match[] matches) {
			view.updateHighlightCount(matches.length);
		}
		
		private void changeManager(IWorkbenchPart part) {
			if (part instanceof IEditorPart) {
				activeManager = (EditorAnnotationManager)managers.get(part);
				if (activeManager == null) {
					activeManager = new EditorAnnotationManager((IEditorPart)part, this);
					if (activeManager.isEnabled())
						managers.put(part, activeManager);
					else
						activeManager = null;
				}
			}
			else
				activeManager = null;
		}
		
		public void selectionChanged(SelectionChangedEvent event) {
			if (mPart != null)
				updateTargetPart(getActiveEditor(mPart), false);
		}
		
		public void highlight(String text, boolean paused) {
			if (activeManager != null && text != null) {
				Pattern pattern = SearchEngine.createPattern(text, matchCase, prefix);
				engine.setMatcher(pattern, prefix, activeManager, paused);
			}
		}

		/*
		 * @see IPartListener#partActivated(IWorkbenchPart)
		 */
		public void partActivated(IWorkbenchPart part) {
			partActivated(part, false);
		}

		/*
		 * @see IPartListener#partClosed(IWorkbenchPart)
		 */
		public void partClosed(IWorkbenchPart part) {
			managers.remove(part);
			if (part == fPart)
				partActivated(null);
		}

		/*
		 * @see DisposeListener#widgetDisposed(DisposeEvent)
		 */
		public void widgetDisposed(DisposeEvent event) {
			if (fWindow != null) {
				fWindow.getPartService().removePartListener(this);
				fWindow = null;
			}
			disposeManagers();
			disposeMultiPart();
			fPart = null;
			fPreviousTarget = null;
		}
		
		private void disposeManagers() {
			Iterator it = managers.values().iterator();
			while (it.hasNext()) {
				EditorAnnotationManager elem = (EditorAnnotationManager) it.next();
				if (elem != null) {
					elem.dispose();
				}
			}
		}

		/*
		 * @see IPartListener#partOpened(IWorkbenchPart)
		 */
		public void partOpened(IWorkbenchPart part) {}

		/*
		 * @see IPartListener#partDeactivated(IWorkbenchPart)
		 */
		public void partDeactivated(IWorkbenchPart part) {}

		/*
		 * @see IPartListener#partBroughtToTop(IWorkbenchPart)
		 */
		public void partBroughtToTop(IWorkbenchPart part) {}
	}

	/**
	 * Updates agent because of a different target.
	 * @param target the new target
	 * @param initializeFindString <code>true</code> if the find string of this dialog should be initialized based on the viewer's selection
	 */
	private void updateTarget(IFindReplaceTarget target, boolean initializeFindString, boolean highlight) {
		this.target = target;
		update(initializeFindString, highlight);
	}
	
	private void update(boolean initializeFindString, boolean highlight) {
		boolean enabled = target != null;
		String text = initializeFindString && enabled ?
				target.getSelectionText() : this.text;
		notFoundText = null;
		view.update(fPart, highlight, text);
	}
	
	private void removeSelection() {
		if (target instanceof IFindReplaceTargetExtension) {
			int offset = target.getSelection().x;
			IFindReplaceTargetExtension eTarget = (IFindReplaceTargetExtension)target;
			eTarget.setSelection(offset, 0);
		}
	}
	
	/** The workbench part */
	private IWorkbenchPart fPart;
	private MultiPageEditorPart mPart;
	/** The previous find/replace target */
	private IFindReplaceTarget fPreviousTarget;
	/** The workbench window */
	private IWorkbenchWindow fWindow;
	
	private ISearchView view;
	private IFindReplaceTarget target;
	/** Last found text */
	private String text;
	/** Last not found text */
	private String notFoundText;
	private boolean matchCase;
	private boolean prefix;
	private boolean highlight;
	private int selectedIndex = 0;
	private AgentListener agentListener;
	private SearchEngine engine;

}
